// Copyright 2010 Google Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.android.apps.moderator;

import java.util.Iterator;
import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.database.DataSetObservable;
import android.database.DataSetObserver;
import android.view.ContextMenu;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ContextMenu.ContextMenuInfo;
import android.view.View.OnCreateContextMenuListener;
import android.widget.AdapterView;
import android.widget.ListAdapter;
import android.widget.ListView;
import android.widget.AdapterView.OnItemClickListener;

/**
 * A mechanism to populate an Activity's ListView with data entries that
 * implement a ListEntry interface. This removes the heavy lifting associated
 * with inflating/recycling views, setting up click and context menu
 * interfaces, and refreshing the ListView when underlying data changes.
 */
class ListSynchronizer {

  /**
   * Compares the content by-reference of two lists
   * @return true if the lists have the same entries
   */
  static private boolean sameList(Iterable<? extends ListEntry> aList,
      Iterable<? extends ListEntry> bList) {
    final Iterator<? extends ListEntry> aIterator = aList.iterator();
    final Iterator<? extends ListEntry> bIterator = bList.iterator();
    while (aIterator.hasNext()) {
      if (!bIterator.hasNext()) {
        return false;
      }
      if (aIterator.next() != bIterator.next()) {
        return false;
      }
    }
    return !bIterator.hasNext();
  }

  /**
   * The parent activity for the synchronizer.
   */
  final private Activity activity;

  /**
   * The BaseAdapter employed by the ListSynchronizer.
   */
  private final ListAdapter baseAdapter = new ListAdapter() {

    public boolean areAllItemsEnabled() {
      return true;
    }

    public int getCount() {
      return entries.size();
    }

    public Object getItem(int position) {
      return getEntry(position);
    }

    public long getItemId(int position) {
      return position;
    }

    public int getItemViewType(int position) {
      ListEntry listEntry = getEntry(position);
      int layoutId = listEntry.getLayoutId();
      int index = views.indexOf(layoutId);
      if (index == -1) {
        throw new RuntimeException("View ID supplied by getLayoutId() not in list of view IDs");
      }
      return index;
    }

    public View getView(int position, View convertView, final ViewGroup parent) {

      ListEntry listEntry = getEntry(position);

      // The list entry supplies the ID of the layout required
      final int layoutId = listEntry.getLayoutId();

      // If the convertView is of the correct type, recycle it
      final View v;
      if (convertView != null && Integer.valueOf(layoutId).equals(convertView.getTag())) {
        v = convertView;
      } else {
        v = inflater.inflate(layoutId, parent, false);
        // The tag is set to the current layout ID, for easy identification later
        // when we are considering recycling
        v.setTag(layoutId);
      }

      // Call on the list entry to customize the inflated/recylced view
      listEntry.customizeView(v, activity);

      return v;
    }

    public int getViewTypeCount() {
      return views.size();
    }

    public boolean hasStableIds() {
      return true;
    }

    public boolean isEmpty() {
      return getCount() == 0;
    }

    public boolean isEnabled(int position) {
      return true;
    }

    public void registerDataSetObserver(DataSetObserver observer) {
      dataSetObservable.registerObserver(observer);
    }

    public void unregisterDataSetObserver(DataSetObserver observer) {
      dataSetObservable.unregisterObserver(observer);
    }
  };

  /**
   * If the list synchronizer is due an visual update
   */
  private boolean dueChange;

  /**
   * The entries currently synchronized to the list
   */
  private List<ListEntry> entries;

  /**
   * A single instance of a layout inflater
   */
  private LayoutInflater inflater;

  /**
   * The listView this synchronizer is set to
   */
  private final ListView listView;

  /**
   * The data set observable belonging to the list adapter
   */
  private final DataSetObservable dataSetObservable = new DataSetObservable();

  /**
   * A list of all layout IDs that entries can have
   */
  private final List<Integer> views;

  public ListSynchronizer(final Activity activity, ListView listView, List<Integer> views) {
    this.activity = activity;
    this.listView = listView;
    this.views = views;
  }

  /**
   * Invoked internally when something in the list has changed and a redraw is required
   */
  void dataChanged() {
    dueChange = true;
    activity.runOnUiThread(new Runnable() {
      public void run() {
        // Several things may trigger this request, fortunately one side effect of running on the
        // UI thread is the delay may allow near-simultaneous change requests to be
        // bundled together.
        if (dueChange) {
          dueChange = false;
          dataSetObservable.notifyChanged();
        }
      }
    });
  }

  /**
   * Returns the ListEntries currently bound to the synchronizer
   */
  public List<ListEntry> getEntries() {
    return entries;
  }

  /**
   * Returns the ListEntry currently being displayed at the specified list index.
   */
  ListEntry getEntry(int position) {
    return entries.get(position);
  }

  /**
   * Hunts for and returns the position of the specified listEntry.
   *
   * @return The position of the entry, or -1 if the entry could not be located
   */
  int getPosition(ListEntry findListEntry) {
    int idx = 0;
    for (ListEntry listEntry : entries) {
      if (listEntry == findListEntry) {
        return idx;
      }
      idx++;
    }
    return -1;
  }

  /**
   * Set or update the ListEntries. This will populate or repopulate the
   * ListView.
   *
   * @param newEntries The entries to populate
   */
  public void setEntries(final List<ListEntry> newEntries) {

    if (setEntriesInternal(newEntries)) {
      dataChanged();
    }
  }

  /**
   * Internal form of setEntries that will not invoke data changed events.
   *
   * @param newEntries The entries to populate
   *
   * @return true if the entries are actually modified
   */
  boolean setEntriesInternal(final List<ListEntry> newEntries) {
    // If the entries are set to 'null' the list is cleared
    if (newEntries == null) {
      entries = newEntries;
      activity.runOnUiThread(new Runnable(){
        public void run() {
          listView.setAdapter(null);
        }});
      return false;
    }

    if (listView.getAdapter() == null) {
      // Adapter initialized the first time we have data
      inflater = (LayoutInflater) activity.getApplicationContext().
          getSystemService(Context.LAYOUT_INFLATER_SERVICE);

      entries = newEntries;
      activity.runOnUiThread(new Runnable() {
        public void run() {
          listView.setOnItemClickListener(new OnItemClickListener() {
            public void onItemClick(AdapterView<?> arg0, View arg1, int position, long arg3) {
              ListEntry listEntry = getEntry(position);
              if (!listEntry.clicked(activity)) {
                listView.showContextMenuForChild(arg1);
              }
            }
          });
          listView.setOnCreateContextMenuListener(new OnCreateContextMenuListener(){
            public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) {
              if (v == listView) {
                AdapterView.AdapterContextMenuInfo info =
                    (AdapterView.AdapterContextMenuInfo) menuInfo;
                ListEntry listEntry = getEntry(info.position);
                listEntry.addContextItems(menu, activity);
              }
            }});
          listView.setAdapter(baseAdapter);
        }
      });
    } else {
      if (sameList(entries, newEntries)) {
        return false; // Would not change list
      }
      entries = newEntries;
    }
    return true;
  }

  /**
   * Select the list entry with the given row index
   */
  void setSelection(final int idx) {
    listView.post(new Runnable() {
      public void run() {
        listView.setSelection(idx);
      }
    });
  }

}